/*
  @licstart  The following is the entire license notice for the
  JavaScript code in this page.

  Copyright (C) 2024 Gong Zhile

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

  @licend  The above is the entire license notice
  for the JavaScript code in this page.
*/

function crc16_ccitt(view, ofs, len, crc = 0, xorout = 0) {
	for (var i = ofs, t; i < ofs + len; i++, crc &= 0xFFFF) {
		t = (crc >> 8) ^ view.getUint8(i);
		t ^= t >> 4;
		crc = (crc << 8) ^ (t << 12) ^ (t << 5) ^ t;
	}
	return crc ^ xorout;
}

function cStrFromView(view, ofs, len) {
	var str = '';
	var byte;

	for (var i = ofs; i < len + ofs; i++) {
		byte = view.getUint8(i);
		if (byte === 0) break;
		str += String.fromCharCode(byte);
	}

	return str;
}

class WS63Serial {
	log(message) {
		if (this.logElem)
			this.logElem.textContent += message;
		else
			console.log(message);
	}

	async _timedRead(promise, timeout) {
		// const timeoutPromise = new Promise((resolve, _) => {
		//     setTimeout(() => {
		// 	this.i.releaseLock();
		// 	this.i = this.tty.readable.getReader();
		// 	resolve({
		// 	    value: new Uint8Array(0),
		// 	    done: false,
		// 	});
		//     }, timeout);
		// });
		// return Promise.race([this.i.read(), timeoutPromise]);
		return this.i.read();
	}

	async _ymodem_blk_xmit(buf, nowait) {
		var max_rexmit = 16;

		while (max_rexmit--) {
			await this.o.write(buf);
			if (nowait) return;

			const { value, done } = await this._timedRead(10);

			if (done)
				reader.releaseLock();

			if (value.includes(0x06))
				return;
		}

		throw new Error("ymodem session lost, timed out");
	}

	/**
	 * Construct a loaderBoot frame.
	 *
	 * @param {number} cmd CMD ID (0x00-0xff)
	 * @param {Uint8Buffer} data PAYLOAD (Max Length: 1024)
	 */
	buildDataFrame(cmd, data) {
		const len = 10 + data.byteLength;
		const cmdbuf = new ArrayBuffer(len);
		const bufView = new DataView(cmdbuf);
		const u8buf = new Uint8Array(cmdbuf);

		// Frame Magic, 0xdeadbeef
		bufView.setUint32(0, 0xdeadbeef, 1);
		// Frame Length
		bufView.setUint16(4, len, 1);
		// Frame Command
		bufView.setUint8(6, cmd);
		bufView.setUint8(7, cmd << 4 | cmd >> 4);
		// Frame Payload, Copy from `data'
		u8buf.set(data, 8);
		// Frame Checksum, CRC16 LE
		bufView.setUint16(
			len - 2,
			crc16_ccitt(new DataView(cmdbuf), 0, len - 2),
			true
		);

		return cmdbuf;
	}

	/**
	 * Send a file buffer over Ymodem protocol.
	 *
	 * @param {String} name the name of the file, maybe irrelevant
	 * @param {Uint8Array} fbuf the buffer containing the *exact* length of data
	 * @param {Callable} pgcb the callback function for progress (this, total, wrote)
	 */
	async sendFileBuf(name, fbuf, pgcb) {
		var blkBuf = new Uint8Array(1029);
		var blkView = new DataView(blkBuf.buffer);
		var xmitLen = "0x" + fbuf.byteLength.toString(16);
		var i_blk = 0, total_blk = Math.ceil(fbuf.byteLength / 1024.0);
		var last_blk = fbuf.byteLength - ~~(~~(fbuf.byteLength / 1024) * 1024) || 1024;
		var waitC = true, crc = 0;

		if (pgcb) pgcb(this, total_blk + 2, i_blk);

		// Stage 0: Wait for C
		while (waitC) {
			const { value, done } = await this._timedRead(10);

			if (done) {
				reader.releaseLock();
				break;
			}

			for (const ch of value) {
				if (ch == 0x43) {
					waitC = false;
					break;
				}

				if (ch == 0x0a || (ch >= 0x20 && ch <= 0x7e))
					this.log(String.fromCharCode([ch]));
			}
		}

		// Block 0: File Info
		blkBuf[0] = 0x01;
		blkBuf[1] = 0x00;
		blkBuf[2] = 0xff;

		for (var i = 0; i < name.length; i++)
			blkBuf[3 + i] = name.charCodeAt(i);

		for (var i = 0; i < xmitLen.length; i++)
			blkBuf[4 + name.length + i] = xmitLen.charCodeAt(i);

		blkView.setUint16(131, crc16_ccitt(blkView, 3, 128), 0);
		await this._ymodem_blk_xmit(blkBuf.subarray(0, 133));

		++i_blk;
		if (pgcb) pgcb(this, total_blk + 2, i_blk);

		// Data Blocks: File Data
		while (i_blk < total_blk + 1) {
			const rlen = (i_blk == total_blk) ? last_blk : 1024;
			if (rlen < 1024) blkBuf.fill(0, 3, 1027);

			blkBuf[0] = 0x02;
			blkBuf[1] = i_blk % 0x100;
			blkBuf[2] = 0xff - blkBuf[1];
			blkBuf.set(fbuf.subarray((i_blk - 1) * 1024, (i_blk - 1) * 1024 + rlen), 3);
			blkView.setUint16(1027, crc16_ccitt(blkView, 3, 1024), 0);

			await this._ymodem_blk_xmit(blkBuf);

			++i_blk;
			if (pgcb) pgcb(this, total_blk + 2, i_blk);
		}

		// EOT
		await this.o.write(new Uint8Array([0x04]));

		// Block 0: Finish Xmit
		blkBuf[0] = 0x01;
		blkBuf[1] = 0x00;
		blkBuf[2] = 0xff;
		blkBuf.fill(0, 3, 131);
		blkView.setUint16(131, crc16_ccitt(blkView, 3, 128), 0);

		// May read the ACK frame as well, so skip it. :-/
		await this._ymodem_blk_xmit(blkBuf.subarray(0, 133), 1);

		++i_blk;
		if (pgcb) pgcb(this, total_blk + 2, i_blk);

		return this.recvDataFrame();
	}

	async recvDataFrame(oneshot = false) {
		const buf = new Uint8Array(1036);
		const bufView = new DataView(buf.buffer);
		const mgc = new Uint8Array([0xef, 0xbe, 0xad, 0xde]);
		var i = 0, framelen = 0, st = 0, wait = true, good = false;

		while (wait) {
			if (oneshot) wait = false;
			const { value, done } = await this._timedRead(10);

			if (done) {
				reader.releaseLock();
				break;
			}

			for (const ch of value) {
				buf[i] = ch;
				switch (st) {
					case 0:
						if (mgc[i] == buf[i]) {
							if (++i >= 3) st = 1;
							continue;
						}

						if (i >= 3) {
							st = 1;
							continue;
						}

						i = 0;

						if (buf[i] == 0x0a || (buf[i] >= 0x20 && buf[i] <= 0x7e))
							this.log(String.fromCharCode([buf[i]]));
						continue;
					case 1:
						if (i == 5)
							framelen = bufView.getUint16(4, 1);
						else if (i == framelen - 1)
							break;
						i++;
						continue;
					default:
						st = 0;
						continue;
				}

				/* TODO: deal with the frame, copy it to somewhere */

				wait = i = st = 0;

				// Is it ACK frame?
				if (buf[6] == 0xe1 || buf[6] == 0x2e)
					good = true;

				/* log the reset of the asciis */
				continue;
			}
		}

		return good;
	}

	/**
	 * Request the MCU to reset (must call this after construct).
	 */
	async reqHandshake(baudRate) {
		var handshake_ok = false, frame;

		if (this.tty.readable) {
			if (this.i)
				this.i.releaseLock();
			if (this.o)
				this.o.releaseLock();
			await this.tty.close();
		}

		await this.tty.open({ baudRate: 115200 });
		this.i = this.tty.readable.getReader();
		this.o = this.tty.writable.getWriter();

		const buf = new Uint8Array([0x00, 0xc2, 0x01, 0x00, 0x08, 0x01, 0x00, 0x00]);
		const view = new DataView(buf.buffer);

		view.setUint32(0, baudRate, true);

		frame = this.buildDataFrame(0xf0, buf);

		while (1) {
			await this.o.write(frame);
			handshake_ok = await this.recvDataFrame(true);
			if (handshake_ok) {
				if (this.i)
					this.i.releaseLock();
				if (this.o)
					this.o.releaseLock();
				await this.tty.close();

				await this.tty.open({ baudRate });
				this.i = this.tty.readable.getReader();
				this.o = this.tty.writable.getWriter();
				return;
			}
		}
	}

	/**
	 * Request the MCU to reset (after loaderBoot loaded).
	 */
	async reqReset() {
		var reset_ok = false, frame;

		frame = this.buildDataFrame(0x87, new Uint8Array([
			0x00, 0x00,
		]));

		await this.o.write(frame);
		return this.recvDataFrame();
	}

	/**
	 * Request the MCU to switch to a new baudrate (after loaderBoot loaded).
	 *
	 * @param {Number} baudRate the requested baudrate for MCU
	 */
	async reqBaudrate(baudRate = 115200) {
		var payload = new Uint8Array(8), view = new DataView(payload.buffer);
		var frame;

		view.setUint32(0, baudRate, 1);
		view.setUint8(4, 0x08);
		view.setUint8(5, 0x01);

		frame = this.buildDataFrame(0x5a, payload);
		await this.o.write(frame);
		await this.recvDataFrame();

		if (this.i)
			this.i.releaseLock();
		if (this.o)
			this.o.releaseLock();
		await this.tty.close();

		await this.tty.open({ baudRate });
		this.i = this.tty.readable.getReader();
		this.o = this.tty.writable.getWriter();
	}

	/**
	 * Request the MCU to recv a bin (after loaderBoot loaded).
	 *
	 * @param {Number} addr the address to be wrote on the MCU
	 * @param {Number} ilen the length of infomation to be transfered
	 * @param {Number} eras the length of infomation to be erased
	 *                      (OPTIONAL, aligned to 2K by default)
	 */
	async reqSendBin(addr, ilen, eras) {
		const binFramePayload = new Uint8Array(14);
		const binFramePayloadV = new DataView(binFramePayload.buffer);
		if (!eras) eras = Math.ceil(ilen / 8192.0) * 0x2000;

		binFramePayloadV.setUint32(0, addr, 1);
		binFramePayloadV.setUint32(4, ilen, 1);
		binFramePayloadV.setUint32(8, eras, 1);

		binFramePayload[12] = 0x00;
		binFramePayload[13] = 0xff;

		const binFrame = this.buildDataFrame(0xd2, binFramePayload);

		await this.o.write(binFrame);
		return this.recvDataFrame();
	}

	/**
	 * Request the MCU to erase the flash. (Actually, a special reqSendbin cmd.)
	 */
	async reqEraseAll() {
		return this.reqSendBin(0x0000, 0x0000, 0xffff);
	}

	/**
	 * Construct a WS63Serial instance for serial communication.
	 *
	 * @param {SerialPort} tty the serial port of the MCU, may not be opened
	 * @param {HTMLElement} logElem the DOM element for logging
	 */
	constructor(tty, logElem) {
		this.tty = tty;
		if (!logElem)
			logELem = document.createElement("pre");
		this.logElem = logElem;
	}

	/**
	 * Release a WS63Serial instance with its serial port. The object may not
	 * be called by all means after releasing.
	 */
	async release() {
		if (this.i)
			this.i.releaseLock();
		if (this.o)
			this.o.releaseLock();
		await this.tty.close();
	}
}

class WS63Fwpkg {
	endian = true
	nbin = 0
	binl = []

	getBinDescription(binName) {
		var binDesc = this.binl.find(desc => desc.name === binName);
		if (!binDesc)
			throw new Error("requested bin does not exist in fwpkg");
		return binDesc;
	}

	getBinUint8Buffer(binName) {
		var binDesc = this.getBinDescription(binName);
		return new Uint8Array(this.data, binDesc.offset, binDesc.length);
	}

	constructor(data) {
		this.data = data;
		this.view = new DataView(data);

		/* Parse the fwpkg headers. */
		const fwpkgMagic = this.view.getUint32(0, this.endian);
		if (fwpkgMagic == 0xefbeaddf)
			this.endian = this.endian;
		else if (fwpkgMagic == 0xdfadbeef)
			this.endian = !this.endian;
		else
			throw new Error("fwpkg bad magic, wrong file perhaps?");

		this.nbin = this.view.getUint16(6, this.endian);

		if (this.view.getUint16(4, this.endian)
			!= crc16_ccitt(this.view, 6, this.nbin * 52 + 6))
			throw new Error("fwpkg corrupted, bad crc");

		for (var i = 0; i < this.nbin; i++) {
			const ofs = 12 + 52 * i;
			this.binl.push({
				name: cStrFromView(this.view, ofs, 32),
				offset: this.view.getUint32(ofs + 32, this.endian),
				length: this.view.getUint32(ofs + 36, this.endian),
				burn_addr: this.view.getUint32(ofs + 40, this.endian),
				burn_size: this.view.getUint32(ofs + 44, this.endian),
				type_2: this.view.getUint32(ofs + 48, this.endian),
			});
		}
	}
}
